# modCalcGUI.py                                 David MacQuigg  ece373  12/05/07
'''Module to provide a Calculator GUI, including an interface for button clicks
and keyboard entries.  Also displays the stack of operands maintained by the
Calculator module.'''

# 0.3 Change to new interface definitions per project specs
# 0.2 Add stack display
# 0.1 Starting with Zelle's calculator (Ch.11 in Python Programming)

from graphics import *  # primitives for the GUI (from John Zelle's excellent
                        # package http://mcsp.wartburg.edu/zelle/python/).
from button import Button

class CalcGUI(GraphWin):
    '''A Calculator GUI with buttons and a register stack display.'''

    def __init__(self, width, height):
        '''Set up a calculator GUI with standard buttons and a stack display.
        Stack has width (number of chars) and height (number of registers).
        Buttons include ('+', 'N', '-', '*', '/', 'I', 'M', digits 0-9, and '.')
        '''
        self.width = width
        self.numRegs = height
        scaleY = 28  # 28 pixels per unit in Y-direction
        pixH = scaleY * (5 + height)        # height in pixels
        pixW = max(250, 10 * width)         # width in pixels
        
        win = GraphWin("calculator", width=pixW, height=pixH)
        win.setCoords(0,0,6,pixH/28)
        win.setBackground("slategray")
        self.win = win
        self._createButtons()
        self._createStackDisplay(width, height)

    def close(self):
        self.win.close()

    def displayLine(self, regnum, strValue):
        '''Refresh the display of register regnum, replacing its current
        contents with strValue.  No checking is done other than to throw an
        IndexError if regnum is out of range, or a TypeError if strValue is not
        a string.  Register 0 is at the bottom.'''
        
        if regnum not in range(self.numRegs):
            raise IndexError, ('Register number must be in range %s' %
                               range(self.numRegs) )

        if not isinstance(strValue, str):
            raise TypeError, 'Displayed value must be a string'

        srr = self.regs[regnum]
        srr.setText(strValue)
        srr.undraw(); srr.draw(self.win)

    def getButton(self):
        ''' Waits for a button to be clicked and returns the label of
        the button that was clicked.  Raises ValueError if there is a keyboard
        interrupt while waiting.'''
        try:
            while True:
                p = self.win.getMouse()  # get next or pending mouse click
                for b in self.buttons:
                    if b.clicked(p):     # ignore clicks not on a button
                        return b.getLabel()
        except:
            print "Caught CTRL-C interrupt"
            return None
       
    def getNext(self):
        '''Returns an item code and an item entered by a button click or by
        keyboard input to the bottom register (reg0).  If a button click, the
        item code is "B", and the item is the label on the button.  If a
        keyboard entry, the item code is whatever string appears ahead of the
        first colon in reg0, and the item is the Python object represented by
        the string to the right of the first colon.
        '''
        while True:
            try:
                b = self.getButton()
                if b == 'E':  # get the latest keyboard entry from reg0
                    sr0 = self.reg0
                    sr0.setFill('white')
                    sr0.undraw(); sr0.draw(self.win)
                    txt = sr0.getText()            # "P: {1:10, 3:30}"
                    parse1 = txt.split(':', 1)     # split on first colon
                    itemcode = parse1[0].strip()   # strip whitespace
                    obj = eval(parse1[1])          # eval the Python object
                    ##### ToDo:  Need to block malicious user input.            
                    return itemcode, obj
                
                else:         # return the label of the button clicked
                    return 'B', b  # itemcode is 'B'
                    
            except:
                sr0.setFill('red')
                sr0.undraw(); sr0.draw(self.win)

    def _createButtons(self):
        '''Create list of buttons.'''
        # bSpecs gives center coords and label of buttons
        bSpecs = [(1,1,'E'), (2,1,'0'), (3,1,'.'), (4,1,'N'), (5,1,'I'),
                  (1,2,'1'), (2,2,'2'), (3,2,'3'), (4,2,'+'), (5,2,'-'),
                  (1,3,'4'), (2,3,'5'), (3,3,'6'), (4,3,'*'), (5,3,'/'),
                  (1,4,'7'), (2,4,'8'), (3,4,'9'), (4,4,'M'), (5,4,'C')]
        self.buttons = []
        for cx,cy,label in bSpecs:
            self.buttons.append(Button(self.win,Point(cx,cy),.75,.75,label))
        # activate all buttons
        for b in self.buttons:
            b.activate()
            
    def _createStackDisplay(self, width, numRegs):
        '''Set up a stack display with given width (number of chars) and given
        height (number of registers).'''
        
        self.stackWidth = width
        self.numRegs = numRegs
        self.regs = []
        for regnum in range(numRegs):
            e = Entry(Point(3, 5.2 + regnum), width)
            e.setText(str(regnum))
            e.setFill('white')
            e.draw(self.win)
            self.regs.append(e)
            if regnum == 0: self.reg0 = e

    def _getLine(self, regnum):
        '''Returns the string in display register regnum.'''
        return self.regs[regnum].getText()

    def _push(self, strValue):
        '''Push a string Value onto the display stack.'''
        top = self.numRegs - 1
        for n in range(top, 0, -1):  # [4, 3, 2, 1]
            txt = self._getLine(n-1)
            self.displayLine(n, txt)
            
        self.displayLine(0, strValue)   

if __name__ == '__main__':

    # Sample data.  Various structures that might be useful for representing
    # polynomials, matrices, etc.
    dict1 = {1:10, 3:30}             # A polynomial
    list1 = [[4,5,6,7],[8,9,10,11]]  # A matrix
    regList = [1,2.1,3+2j,dict1,list1]    # 5 items here

    # Create a CalcGUI object with 5 registers
    gui = CalcGUI(50, 5)

    while True:              # event loop
        itemcode, obj = gui.getNext()       # get input
        if obj == None:
            break
        print itemcode, obj
        result = '%s: %s' % (itemcode, obj) # do stuff
        gui._push(result)                   # show results

    ans = raw_input('Close the GUI? (y/n): ')
    if ans == 'y':
        gui.close()


''' ToDo:
Need to block malicious user input to eval function in getNext
Need to catch window close event
'''

